import _ from 'lodash';
import * as Minio from 'minio';

import { ObjectStorage } from './objectStorage';
import { Readable as ReadableStream } from 'stream';
import { logger, convertStreamToString, convertStreamToBuffer, promiseRetry } from '$common';
export interface MinioConfig {
  host: string,
  port: number,
  useSSL: boolean,
  accessKey: string,
  accessSecret: string,
  bucket?: string
}

export class MinioStorage implements ObjectStorage {

  private readonly defaultBucketName = 'suanpan';

  private bucket: string;
  private minioClient: Minio.Client;

  constructor(config: MinioConfig) {
    this.minioClient = new Minio.Client({
      endPoint: config.host, 
      port: config.port, 
      useSSL: config.useSSL, // If set to true, https is used instead of http. Default is true.
      accessKey: config.accessKey,
      secretKey: config.accessSecret
    });
    this.bucket = config.bucket || this.defaultBucketName;
  }

  async fGetObject(objectName: string, filePath: string): Promise<void> {
    try {
      await this.minioClient.fGetObject(this.bucket, objectName, filePath);
    } catch (e) {
      logger.error(`❗️MinioStorage ==> fGetObject failed. bucket: ${this.bucket}, objectName: ${objectName}, localFilePath: ${filePath}`);
      throw e;
    }
  }

  async checkObjectNameExist(objectName: string) {
    let bucketExist = false;
    try {
      await this.getObjectToStream(objectName);
      bucketExist = true;
    } catch (ex) {
      if (ex.code && !_.includes(['NoSuchKey', 'NotFound'], ex.code)) {
        throw ex;
      }
    }
    return bucketExist;
  }

  async getObjectToBuffer(objectName: string): Promise<Buffer> {
    const stream = await this.getObjectToStream(objectName);
    return convertStreamToBuffer(stream);
  }

  async getObjectToStream(objectName: string) {
    return await this.minioClient.getObject(this.bucket, objectName);
  }

  async getObjectToString (objectName: string) {
    const stream = await this.getObjectToStream(objectName);
    return await convertStreamToString(stream);
  }

  async fPutObject(objectName: string, filePath: string) {
    await promiseRetry(async (retry, times: number) => {
      try {
        await this.minioClient.fPutObject(this.bucket, objectName, filePath, {});
      } catch (ex) {
        if (ex.code !== 'ECONNRESET') { throw ex; }
        logger.info('The Minio server shuts down TCP before the client');
        logger.info(`retry ${times} times`);
        retry(ex);
      }
    });
  }

  async putObject(objectName: string, data: ReadableStream | Buffer | string) {
    await this.minioClient.putObject(this.bucket, objectName, data);
  }

  async putObjectWithMetaData(objectName: string, data: ReadableStream | Buffer | string, metaData: object) {
    await this.minioClient.putObject(this.bucket, objectName, data, metaData);
  }

  async deleteObject(objectName: string) {
    await this.minioClient.removeObject(this.bucket, objectName);
  }

  async deleteMultiObjects(objectNames: string[]) {
    await this.minioClient.removeObjects(this.bucket, objectNames);
  }

  async append(objectName: string, appendContent: string): Promise<any> {
    let originalContent = '';
    try {
      originalContent = await this.getObjectToString(objectName);
    } catch (e) {
      logger.debug(`❗️Storage AppendWarning ==> ${e.code}: ${e.message} objectName: ${objectName}`);
    }
    const data = `${originalContent}${appendContent}`;
    await this.putObjectWithMetaData(objectName, data, { 'Content-Disposition': 'inline', 'Content-Type': 'text/plain' });
  }

  async getLastModified(objectName: string): Promise<Date> {
    const stat = await this.minioClient.statObject(this.bucket, objectName);
    return stat.lastModified;
  }
}